from machine import *
import shutil
import re, subprocess
import sys
from signature import *

class BuildException(Exception):
  pass

class NotImplementedException(Exception):
  pass

class Builder( object ):
  def __init__(self, runtime ):
    self.runtime = runtime
    self.target = runtime

  def write(self, txt):
    print "[" + self.runtime.fullname() + "]", txt

  def execute( self, command, env = None):
    #if os.system( command ) != 0:
    #  raise Exception("Execution failed on command: " + command )
    commandstr = command
    if isinstance(command, list):
      commandstr = " ".join(  command )
    self.write( commandstr )

    process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env, shell=self.runtime.config.machine.isLinux())
    linebuffer = []

    while process.poll() is None:
      line = process.stdout.readline()
      if line:
        linebuffer.append(line)
        if len(linebuffer) > self.runtime.config.LINEBUFFERSIZE:
          linebuffer.pop(0)

        if self.runtime.config.verbose:
          self.write(line.rstrip())
        else:
          sys.stdout.write( '.' )

    sys.stdout.write("\n")

    returncode = process.poll()
    if returncode != 0:
      msg = "Error occurred. Last %s lines: \n\n" % self.runtime.config.LINEBUFFERSIZE
      msg += "".join(linebuffer)
      raise BuildException(msg)



  def copydir(self, source, target):
    if not os.path.exists(target):
      shutil.copytree(source, target)
    else:
      # Merge the directory into the existing dir.
      def subcopy(target, dirname, fnames):
        for f in fnames:
          tgt = os.path.join(target, f)
          src = os.path.join(dirname, f)
          if os.path.isdir(src):
            if not os.path.exists(tgt):
              os.makedirs(tgt)
          else:
            shutil.copy2( src, tgt )
      os.path.walk(source, subcopy, target)

  def copyfile( self, source, target ):
    if not os.path.exists(target):
      os.makedirs(target)
    shutil.copy2( source, target )

  def copypattern( self, sourcedir, pattern, target ):
    if not os.path.exists(target):
      os.makedirs(target)

    for file in os.listdir(sourcedir):
      if re.match( pattern, file ) is not None:
        shutil.copy2( sourcedir + "/" + file, target )

  def deletepattern(self, sourcedir, pattern):
    for file in os.listdir(sourcedir):
      if re.match( pattern, file ) is not None:
        os.unlink( sourcedir + "/" + file)
        
  def replaceInFile( self, input_file, replacements ):
       """
          replace all the occurances of the first with the second tuple items
          replaceInFile([ (replace_this, with_this), (replace_that, with_that) ])
       """
       file = open(input_file, "rU")
       text = file.read()
       file.close()

       for rep in replacements:
         text = text.replace(rep[0], rep[1])
       
       file = open(input_file, "w")
       file.write(text)
       file.close()
       
    
  def replaceLinesInFile( self, input_file, patterns ):
    if os.path.exists(input_file + ".bak"):
      os.remove( input_file )
      os.rename( input_file + ".bak", input_file )
    temp_file = input_file + ".tmp" 
    input = open(input_file, 'rU')
    output = open(temp_file, 'w')

    for s in input.xreadlines():
      matched = False
      for pat in patterns:
        if re.match( pat[0], s ):
          output.write(pat[1] + "\n" )
          matched = True
      if not matched:
        output.write( s ) 

    input.close()
    output.close()
    try:
      os.unlink( input_file + ".bak"  )
    except:
      pass

    os.rename( input_file, input_file + ".bak" )
    os.rename(temp_file, input_file)

  def convertEOLStyle( self, filename ):
    if not os.path.exists(filename+".bak2"):
      shutil.copyfile(filename, filename+".bak2")
    
      with open(filename, 'rU') as fp:
        data = fp.read()

      with open(filename, "w" ) as fp:
        fp.write( data )

  def cleandir(self, directory ):
   if os.path.exists(directory):      
     shutil.rmtree( directory )

   if not os.path.isdir( directory ):
     os.makedirs( directory )


class CheckException(Exception):
  pass
  
class Check( object ):
  def __init__(self ):
    super(Check, self).__init__()

  def isvalid( self, executable, message ):
    try:
      x = self.executeoutput( executable )
      if len(x) > 0:
        return
    except:
      pass
    raise CheckException(message)


  def executeoutput (self, command, env=None):
    process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env)
    return process.stdout.read()


  def check(self):
    pass

  def process(self):
    self.check()



###############################################################

class GenericBuilder( Builder ):
  def __init__(self, runtime ):
    super(GenericBuilder, self).__init__(runtime)

    # Old
    self.machine = self.runtime.config.machine
    self.directories = self.runtime.config.directories
    self.url = runtime.url
    self.extractedpath = None
    self.rebuilds = self.runtime.config.rebuilds
    self.debug = self.runtime.debug
    self.build_linktype = self.runtime.build_linktype

    self.dependencies = self.runtime.dependencies #procdeps
    self.version = self.runtime.build_version

  def hasdebug(self):
    return True

  def archivepath(self):
    # BIG FAT WARNING: use self.target instead of self.runtime.target. 
    # target may change and renders the dependencies useless.
    # self.dependencies["dep"].getlibdir() may fail.
    return os.path.join( self.runtime.config.directories.download, self.runtime.url.packagearchive() )

  # Return a path (string) where the final installation will be placed.
  # e.g. enviro/Linux64/osg-1.2.3
  def targetpath(self):
    # BIG FAT WARNING: use self.target instead of self.runtime.target. 
    # target may change and renders the dependencies useless.
    # self.dependencies["dep"].getlibdir() may fail.
    return os.path.join( self.runtime.config.directories.enviro, self.runtime.url.packagedir() )

  # Return a download and extract object.
  def downloadextract(self):
    return DownloadExtract()

  # Return the path to the installation. By default return the same path as the targetpath.
  def installpath(self):
    return self.targetpath()

  def builderconfig(self):
    pass

  def patch( self ):
    pass

  def configure( self ):
    pass

  def make( self ):
    pass

  def install( self ):
    pass

  def copy( self ):
    pass 
    #print "deleting targetpath before copy: ", self.targetpath
    #shutil.rmtree( self.targetpath )
    #print "copying: ", self.installpath, self.targetpath
    #self.copydir( self.installpath, self.targetpath )

  def printInfo(self ):

    if self.debug:
      print self.target.url.packagedir(), "Debug Version"
    else:
      print self.target.url.packagedir()


  def getbindir(self):
    return os.path.join(self.targetpath(), "bin")

  def getlibdir(self):
    return os.path.join(self.targetpath(), "lib")

  def getincludedir(self):
    return os.path.join(self.targetpath(), "include")

  def process(self ):
    import inspect  
    self.builderfile = inspect.getsourcefile(inspect.getmodule(self))
    
    # check the signature, use release signature if we cannot build a debug version.
    dbg = "release"
    if self.hasdebug(): 
      dbg = "debug" if self.debug else "release"
    signfile = os.path.sep.join([self.targetpath(), "buildext.%s" % dbg ])
    signature = Signature( signfile, [self.builderfile] )
    
    # If the signature is not valid, recompile.
    if not os.path.exists(signfile) or (self.rebuilds and not signature.isvalid()):

      self.builderconfig()


      self.write( "===== Download and Extract" )
      self.extractedpath = self.downloadextract().fetch( self.runtime.url.url, downloadlocation=self.archivepath(), buildlocation=self.directories.build )

      if( self.installpath() != self.targetpath() ):
        self.write( "===== Cleanup install path")
        self.cleandir( self.installpath() )

      if self.runtime.cleartarget:
        self.write( "===== Cleanup target path" )
        self.cleandir(self.targetpath())
      else:
        self.write( "===== Skip cleanup target path. This build should be merged")
   
      self.write( "===== Patch")
      self.patch( )
 
      self.write( "===== Configure")
      self.configure( )
  
      self.write( "===== Make")
      self.make( )
  
      self.write( "===== Install")
      self.install( )
  
      self.write( "===== Copy" )
      self.copy()

      self.write( "===== Update signature" )
      signature.resign()
    else:
     self.write( "up-to-date" )
       
        
  def setDebug(self, debug):
    """indicate that the build should contain debug information (used in CMakeBuilder) """
    self.debug = debug

  def dos2unix(self, filename):
    fp = open( filename, "r" )
    data = fp.read()
    fp.close()

    fp = open( filename, "wb" )
    fp.write( data.replace( "\r\n", "\n" ) )
    fp.close()

#######################################


class VisualStudioBuilder( GenericBuilder ):

  def getVisualStudioLibs(self):
    append = ""
    if self.machine.bits == 64:
      append = "\\amd64"
    r = []
    r.append( os.environ["VCInstallDir"] + "\\" + "lib" + append )
    r.append( os.environ["VCInstallDir"] + "\\" + "PlatformSDK\\lib" + append )
    r.append( os.environ["VSInstallDir"] + append)
    r.append( os.environ["VSInstallDir"] + "\\" + "lib" + append )
    r.append( os.environ["FrameworkSDKDir"] + "\\" + "lib" + append )
    return r

  def getVisualStudioIncludes(self):
    r = []
    r.append( os.environ["VCInstallDir"] + "\\" + "include" )
    r.append( os.environ["VCInstallDir"] + "\\" + "PlatformSDK\\include" )
    r.append( os.environ["FrameworkSDKDir"] + "\\" + "include" )
    return r
  
  def getx64replacements(self):
    replacements = []
    if self.machine.bits == 64:
      replacements.append(( ".*Name=\"Release\\|Win32\"", "Name=\"Release|x64\"" ))
      replacements.append(( ".*Name=\"Debug\\|Win32\"", "Name=\"Debug|x64\"" ))      
      replacements.append(( ".*Name=\"ReleaseSingleDLL\\|Win32\"", "Name=\"ReleaseSingleDLL|x64\"" ))
      replacements.append(( ".*Name=\"DebugSingleDLL\\|Win32\"", "Name=\"DebugSingleDLL|x64\"" ))
      replacements.append(( ".*Name=\"Win32\"", "Name=\"x64\"" ))
      replacements.append(( ".*Name=\"VCMIDLTool\"", "Name=\"VCMIDLTool\"\n TargetEnvironment=\"3\""))
      replacements.append(( ".*TargetEnvironment=\"1\"", ""))
      replacements.append(( ".*TargetMachine=\"1\"", ""))
    return replacements

  def convertProjTo64(self, vcproj):
    self.replaceLinesInFile( vcproj, self.getx64replacements() )

  def convertSolutionTo64(self, solution):  
    self.replaceInFile( solution, [ ("Win32", "x64") ] )

  def convertDspToVcProj( self, dsp_filename ):
    self.convertEOLStyle( dsp_filename )
    self.execute("vcbuild /upgrade "+dsp_filename)

  def call_devenv( self, solution, vcproj, osenv=None ):
    """call devenv using the custom environment (if set) for the (additional) include and the lib directories"""
    #if osenv != None:
    #if self.debug:
    dbgstr = "Debug" if self.debug else "Release"
    self.execute(["devenv.com", solution, "/Rebuild", dbgstr, "/project", vcproj, "/projectconfig", dbgstr, "/useenv"], env=osenv )
    #else:  
    #  self.execute(["devenv.com", solution, "/Rebuild", "Release", "/project", vcproj, "/projectconfig", "Release", "/useenv"], env=osenv )
    #else:
    #  if self.debug:
    #    self.execute(["devenv.com", solution, "/Rebuild", "Debug", "/project", vcproj, "/projectconfig", "Debug"] )
    #  else:
    #    self.execute(["devenv.com", solution, "/Rebuild", "Release", "/project", vcproj, "/projectconfig", "Release"] )
         
     
############################

class ConfigureMakeBuilder( GenericBuilder ):
  def hasdebug(self):
    return False

  def configureparameters(self):
    return []

  def configure(self):
    os.chdir( self.extractedpath )
    parameters = self.configureparameters()
    self.execute( "./configure --prefix " + self.installpath() + " " + " ".join( parameters ) )

  def make(self):
    os.chdir( self.extractedpath )
    if self.machine.cores > 1:
      self.execute( "make -j " + str(self.machine.cores) )
    else:
      self.execute( "make" )


  def install(self ):
    os.chdir( self.extractedpath )
    self.execute( "make install" )
      
############################

class NMakeBuilder( GenericBuilder ):
  def makefile(self):
    return "Makefile"

  def make(self ):
    os.chdir( self.extractedpath ) 
    self.execute( "nmake /f " + self.makefile() )


############################

class CMakeBuilder( GenericBuilder ):
#  def setgenerator(self): #    raise Exception("No generator set")
  def setgenerator(self):
    """default generator using machine.buildarch"""
    print self.machine.buildarch
    if self.machine.buildarch == "win32":
      self.generator = "Visual Studio 8 2005"
    elif self.machine.buildarch == "em64t-win":
      self.generator = "Visual Studio 8 2005 Win64"
    elif self.machine.buildarch == "win32_vs11":
      self.generator = "Visual Studio 11"       
    elif self.machine.buildarch == "win64_vs11":
      self.generator = "Visual Studio 11 Win64"
    elif self.machine.buildarch == "Windows32" or self.machine.buildarch == "win32_vs9sp1":
      self.generator = "Visual Studio 9 2008"       
    elif self.machine.buildarch == "Windows64" or self.machine.buildarch == "win64_vs9sp1":
      self.generator = "Visual Studio 9 2008 Win64"
    else:
      self.generator = "Unix Makefiles"

  def configure( self, additional_args="" ):
     #note this is to add cmake to the path if cmake is build (currently cmake should be installed, this
     #is for machines without cmake)
     os.environ["PATH"] = os.environ["PATH"]+os.pathsep+self.directories.enviro+os.sep+"cmake-2.8.4"+os.sep+"bin"
     
     os.chdir( self.extractedpath )

     if os.path.exists( "CMakeCache.txt" ):
       os.unlink( "CMakeCache.txt" )
     self.setgenerator()
     if self.debug:
       self.execute( "cmake . "+additional_args+" -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=" + self.installpath()  + " -G\"" + self.generator + "\"")
     else:
       self.execute( "cmake . "+additional_args+" -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=" + self.installpath()  + " -G\"" + self.generator + "\"")

       
  def makeUnixMakefiles(self):
    self.execute( "make install -j " + str(self.machine.cores) )
  
  def make_general(self):
     if self.debug:
       self.execute( "cmake --build . --target INSTALL --config Debug" )
     else:
       self.execute( "cmake --build . --target INSTALL --config Release" )
       
  def make(self):
    os.chdir( self.extractedpath )
    if self.generator == "Unix Makefiles":
      self.makeUnixMakefiles()
    else: #self.generator.startswith("Visual Studio"):
      self.make_general()

  def install(self):
    pass

#  def install_dir( self ):
#    return self.finished
#
#  def include_dir( self ):
#    return self.finished + "/include"
#
#  def library_dir( self ):
#    if os.path.exists( self.finished + "/lib64" ):
#      return self.finished + "/lib64"
#    else:
#      return self.finished + "/lib"
#
#  def binary_dir( self ):
#    return self.finished + "/bin"
#
#  def document_dir( self ):
#    return self.finished + "/doc"
#
#  def share_dir( self ):
#    return self.finished + "/share"

############################

class QMakeBuilder( GenericBuilder ):

  def hasdebug(self):
    return True

  def configureparameters(self):
    return []

  def configure(self ):
    os.chdir( self.extractedpath )
    parameters = self.configureparameters()

    qtdir = self.dependencies["qt4osmin"].targetpath()
    os.environ['QTDIR'] = qtdir
    os.environ['PATH'] =  os.pathsep.join([qtdir+os.sep+"bin", os.environ['PATH']])

    self.execute( os.path.join(qtdir, "bin", "qmake") + " " + " ".join(parameters) )

  def make(self):
    os.chdir( self.extractedpath )

    qtdir = self.dependencies["qt4osmin"].targetpath()
    os.environ['QTDIR'] = qtdir
    os.environ['PATH'] =  os.pathsep.join([qtdir+os.sep+"bin", os.environ['PATH']])

    if self.machine.isLinux():
      self.execute( "make -j " + str(self.machine.cores) )
    else:
      self.execute("nmake")

  def install(self ):
    os.chdir( self.extractedpath )

    if self.machine.isLinux():
      self.execute( "make install" )
    else:
      self.execute( "nmake install" )


############################

class CMakeJomBuilder( CMakeBuilder ):
  """ subclass of CMakeBuilder specifically for jom building on windows, uses normal CMakeBuilder on linux """
  def setgenerator(self):
    """default generator using machine.buildarch"""
    if self.machine.isLinux():
      CMakeBuilder.setgenerator(self)
    else:
      self.generator = "NMake Makefiles JOM"

  def make(self):
    os.chdir( self.extractedpath )
    if self.machine.isLinux():
      CMakeBuilder.make(self)
    else:
      self.execute(self.dependencies["jom"].jomexecutable())

  def install(self ):
    os.chdir( self.extractedpath )
    if self.machine.isLinux():
      CMakeBuilder.install(self)
    else:
      self.execute( "nmake.exe install/fast" )


############################

#Todo add the nmake -j calls
class BjamBuilder( GenericBuilder ):
  def makeparameters(self):
    return []

  def installparameters(self):
    return []

  def make(self):
    os.chdir( self.extractedpath )
    if not self.bjamexecutable:
      raise Exception ("Define self.bjamexecutable in the subclass" )
    self.execute( self.bjamexecutable + " " + " ".join( self.makeparameters() ) )

  def install(self):
    os.chdir( self.extractedpath )
    self.execute( self.bjamexecutable + " install " + " ".join( self.installparameters() ) )


############################

class DownloadAndCopyBuilder( GenericBuilder ):
  def copyfromsubdir(self):
    return  False

  def hasdebug(self):
    return False
    
  def install( self ):
    if os.path.exists( self.targetpath() ):
      shutil.rmtree( self.targetpath() )

    sourcedir = self.extractedpath
    if self.copyfromsubdir():
      sourcedir += os.sep + os.sep.join( self.copyfromsubdir().split("/") )

    self.copydir( sourcedir, self.targetpath() )


###################

class PythonPackageBuilder(GenericBuilder):
  def configure(self):
    tgt = self.targetpath()
    if not os.path.exists( tgt ):
      os.makedirs( tgt )

    with open(os.path.join(tgt, "readme.txt"), 'w') as fp:
        fp.write("This directory is a place holder.\n")
        fp.write("The module is installed in the python package.\n")

  def install(self):
    os.chdir(self.extractedpath)
    pythonexec = self.dependencies["python"].getbindir() + os.sep + "python"
    self.execute( pythonexec + " setup.py install --prefix="+self.dependencies["python"].targetpath() )

  def hasdebug(self):
    return False


#######################################
# Installers
#######################################

class Installer( Builder ):
  def __init__(self, enviro_dir, package_name, working_dir = None):
    self.enviro_dir = enviro_dir
    self.library_dir = enviro_dir + "/" + package_name + "/lib"
    self.binary_dir = enviro_dir + "/" + package_name + "/bin"
    self.include_dir = enviro_dir + "/" + package_name + "/include"
    self.document_dir = enviro_dir + "/" + package_name + "/doc"
    self.share_dir = enviro_dir + "/" + package_name + "/share"
    self.src_dir = enviro_dir + "/" + package_name + "/src"
    self.working_dir = working_dir
    self.original_dir  = os.path.abspath(os.curdir)
    self.package_name = package_name

  def copylibraries( self, files ):
    if files[0].endswith("lib64"):
      self.library_dir += '64'
    if not os.path.exists( self.library_dir  ):
      os.makedirs( self.library_dir )

    if self.working_dir is not None:
      os.chdir( self.working_dir )

    print( "Copying library files: ")
    for f in files:
      print f, "->", self.library_dir
      if os.path.isdir( f ):
        self.copydir( f, self.library_dir )
      else:
        self.copyfile( f, self.library_dir )

    os.chdir( self.original_dir )


  def copycontent( self, files, contentdir, contentname ):
    if not os.path.exists( contentdir  ):
      os.makedirs( contentdir )

    if self.working_dir is not None:
      os.chdir( self.working_dir )

    print( "Copying "+contentname+" : ")
    for f in files:
      print f, "->", contentdir
      if os.path.isdir( f ):
        self.copydir( f, contentdir )
      else:
        self.copyfile( f, contentdir )

  def copyincludes( self, files ):
    self.copycontent( files, self.include_dir, "include files"  )

  def copybinaries( self, files ):
    self.copycontent( files, self.binary_dir, "executable files"  )

  def copydocuments( self, files ):
    self.copycontent( files, self.document_dir, "documentation"  )

  def copyshare( self, files ):
    self.copycontent( files, self.share_dir, "share folder"  )

  def copysrc( self, files ):
    self.copycontent( files, self.src_dir, "src directory"  )

  def copyinstallation( self, files):
    self.copycontent( files, self.enviro_dir+"/"+self.package_name, "self.package_name"  )

#######################################
# Manifest Tool
#######################################

class ManifestTool( Builder ):
  def __init__(self, working_dir ):
    self.working_dir = working_dir
    self.original_dir  = os.path.abspath(os.curdir)

  def dllpatch( self, dll_file, manifest_file  ):
    os.chdir( self.working_dir )
    self.execute( "mt.exe -manifest "+ manifest_file +" -outputresource:" + dll_file + ";2" )
    os.chdir( self.original_dir )

